package brian.android.mvc.bean;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import android.app.Activity;
import android.util.Log;
import android.view.View;
import brian.android.mvc.annotation.Param;
import brian.android.mvc.exception.BeanCreationException;
import brian.android.mvc.exception.NoBeanIdException;

public class BeanFactory {
	private static volatile BeanFactory instance;
	private boolean isInitialized;
	private Map<String, Object> beanCache = new ConcurrentHashMap<String, Object>();
	private BeanConfig beanConfig;
	
	private BeanFactory() {
	}
	
	/**
	 * @return {@link BeanFactory} 를 반환한다. 
	 * */
	public synchronized static BeanFactory getInstance() {
		if (instance == null) {
			instance = new BeanFactory();
		}
		return instance;
	}
	
	@SuppressWarnings("unchecked")
	public static <T> T getBean(String beanId) throws NoBeanIdException, BeanCreationException {
		if (!getInstance().isInitialized()) {
			forceInit();
		}
		
		//TODO 여기서 deadlock detect 필요 
		return (T) getInstance().getBeanInternal(beanId);
	}
	
	/**
	 * 텅빈 Factory로 초기화한다. 
	 * */
	private static synchronized void forceInit() {
		init(new DefaultBeanConfig());
	}

	public static void setBeanConfig(Class<? extends BeanConfig> beanConfigClass) {
		if (!getInstance().isInitialized()) {
			try {
				init((BeanConfig) beanConfigClass.newInstance());
			} catch (Exception e) {
				Log.e(BeanFactory.class.getSimpleName(), e.getMessage());
			}
		} else {
			Log.d(BeanFactory.class.getSimpleName(), "bean factory already initialized");
		}
	}
	
	/**
	 * BeanFactory 설정을 로딩한다.
	 * */
	private static void init(BeanConfig config) {
		if (!config.isInitialized()) {
			config.initBeans();
		}
		getInstance().isInitialized = true;
		getInstance().beanConfig = config;
	}
	
	public boolean isInitialized() {
		return isInitialized;
	}

	public synchronized Object getBeanInternal(String beanId) throws NoBeanIdException, BeanCreationException {
		if (!isBeanExists(beanId)) {
			throw new NoBeanIdException("bean id \"" + beanId + "\" not exists");
		}
		
		Object bean = null;
		if (isBeanCached(beanId)) {
			bean = getCachedBean(beanId);
		} else {
			bean = getNewBean(beanId);
		}
		
		if (isSingletonBean(beanId)) {
			cacheBean(beanId, bean);
		}
		
		return bean;
	}

	public void cacheBean(String beanId, Object bean) {
		beanCache.put(beanId, bean);
	}

	public boolean isSingletonBean(String beanId) {
		BeanConstructor beanContstuctor = beanConfig.getBeanConstructor(beanId);
		return beanContstuctor.isSingleTon();
	}

	@SuppressWarnings("unchecked")
	private Object getNewBean(String beanId) throws BeanCreationException {
		BeanConstructor beanConstructor = beanConfig.getBeanConstructor(beanId);
		
		try {
			Constructor constructor = getConstructor(beanConstructor);
			Object bean = constructor.newInstance(beanConstructor.getConstructorParameters());
			return bean;
		} catch (Exception e) {
			throw new BeanCreationException("can't create bean id ["+ beanId +"]", e);
		}
	}

	@SuppressWarnings("unchecked")
	private Constructor getConstructor(BeanConstructor beanConstructor) {
		Class beanType = beanConstructor.getBeanType();
		Constructor constructor = null;
		
		try {
			constructor = beanType.getConstructor(beanConstructor.getConstructorParameterTypes());
		} catch (SecurityException e) {
			//do nothing
		} catch (NoSuchMethodException e) {
			//do nothing
		}
		
		if (constructor != null) {
			return constructor;
		}
		
		Class[] targetTypes = beanConstructor.getConstructorParameterTypes();
		for (Constructor cnst : beanType.getConstructors()) {
			Class[] candidateTypes = cnst.getParameterTypes();
			
			if (targetTypes.length == candidateTypes.length) {
				if (targetTypes.length == 0) {
					return cnst;
				}
				
				boolean isMatch = true;
				for (int i=0; i<targetTypes.length; i++) {
					Class targetType = targetTypes[i];
					Class candidateType = candidateTypes[i];
					
					if (!candidateType.isAssignableFrom(targetType)) {
						isMatch = false;
						break;
					}
				}
				
				if (isMatch) {
					return cnst;
				}
			}
		}
		return null;
	}

	private Object getCachedBean(String beanId) {
		return beanCache.get(beanId);
	}

	private boolean isBeanCached(String beanId) {
		return beanCache.containsKey(beanId);
	}

	public boolean isBeanExists(String beanId) {
		if (!getInstance().isInitialized()) {
			forceInit();
		}
		return getInstance().getBanConfig().containsKey(beanId);
	}
	
	public BeanProxy addBean(String beanId, BeanConstructor beanConstructor) {
		return getInstance().getBanConfig().addBean(beanId, beanConstructor);
	}
	
	public BeanConfig getBanConfig(){
		if (!isInitialized()) {
			forceInit();
		}
		
		return beanConfig;
	}

	/**
	 * {@code getArguments} 와 동일하며 argument가 1개 일 때 편하게 사용하려고 만들었음
	 * */
	@SuppressWarnings("unchecked")
	public static List<Object> getArguments(Method method, Activity activity, Object argument, Class argumentType) {
		return getArguments(method, activity, new Object[]{argument}, new Class[]{argumentType});
	}
	
	/**
	 * Method의 Arguments 들을 반환한다. @Param이 있을 경우 view 목록, BeanFactory ID에서 찾고
	 * 파라미터가 없을 경우 {@code arguments} 에서 type 으로 찾는다. 
	 * 목록에서 찾지 못해도 에러는 내지 않고 Warning 만 남긴다. 
	 * */
	@SuppressWarnings("unchecked")
	public static List<Object> getArguments(Method method, Activity activity, Object[] arguments, Class[] argumentTypes) {
		Annotation[][] annotationss = method.getParameterAnnotations();
		
		if (annotationss.length > 0) {
			List<Object> params = new ArrayList<Object>();
			
			for (int i=0; i<annotationss.length; i++) {
				Annotation[] annotations = annotationss[i];
				if (annotations.length > 0) {
					for (Annotation annotation : annotations) {
						if (annotation instanceof Param) {
							Param aParam = (Param) annotation;
							if (aParam.value() > 0) {
								int viewId = aParam.value();
								View view = activity.findViewById(viewId);
								params.add(view);
							} else if (aParam.beanId().length() > 0) {
								try {
									if (getBean(aParam.beanId()) != null) {
										Object bean = getBean(aParam.beanId());
										params.add(bean);
									} else {
										//TODO 여기서 그런 Bean 없은 Exception
									}
								} catch (NoBeanIdException e) {
									//TODO 여기서 Exception Wrapping
								} catch (BeanCreationException e) {
									//TODO 여기서 Exception Wrapping
								}
							}
						}
					}
				} else {
					// annotation 이 없을 때 Event 발생한 View 가 assignable 한 변수면 eventView 할당한다. 
					Class paramType = method.getParameterTypes()[i];
					for (int j=0; j<argumentTypes.length; j++) {
						Class arumentType = argumentTypes[j];
						
						if (paramType.isAssignableFrom(arumentType)) {
							params.add(arguments[j]);
						} 
					}
				}
			}
			return params;
		}
		return Collections.emptyList();
	}
}
